#include <stdbool.h>
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>

#include "merge.h"
#include "termino_merge.h"
#include "buffer_merge.h"
#include "cola.h"
#include "abb_avl.h"
#include "heap.h"
#include "utilidades.h"

// Tamanios en MiB
#define TAM_PARTICION 30
#define TAM_BUFFER 100

/** ***********							*********** **
 ** ***********	ESTRUCTURAS AUXILIARES	*********** **
 ** ***********							*********** **/					

/*
 * Crea y devuelve una lista de n colas. Devuelve la lista de colas,
 * o NULL si no pudo crear alguna de ellas.
 */
cola_t** crear_lista_de_colas(unsigned long numero_de_colas){
		
	cola_t** lista_colas = (cola_t**) malloc ( numero_de_colas*sizeof(cola_t*) );
	
	cola_t* nueva_cola = NULL;	
	
	unsigned long i;
	
	for( i=0 ; i<numero_de_colas ; i++ ) {
		
		nueva_cola = cola_crear();
		
		if( !nueva_cola ) return NULL;
		
		lista_colas[i] = nueva_cola;
		
	}
	
	return lista_colas;
	
}

void wrapper_destruir_termino_merge(void* term){
	termino_merge_t* termino = (termino_merge_t*)term;
	termino_merge_destruir(termino);
}

/*
 * Destruye una lista de n colas.
 */
void destruir_lista_de_colas(cola_t** lista_colas, unsigned long numero_de_colas){
		
	unsigned long i;
	
	for( i=0 ; i<numero_de_colas ; i++ )
	
		cola_destruir(lista_colas[i],wrapper_destruir_termino_merge);
		
	free(lista_colas);
	
}

/*
 * Funcion de comparacion utilizada por el heap.
 */
int comparar_claves(const void* termino1,const void* termino2){
		
	termino_merge_t* t1 = (termino_merge_t*)termino1;
	termino_merge_t* t2 = (termino_merge_t*)termino2;
	
	return strcmp(termino_merge_obtener_string(t2),termino_merge_obtener_string(t1));
	
}

/** ***********							*********** **
 ** ***********		MERGE DE ARCHIVOS	*********** **
 ** ***********							*********** **/

/* 
 * Hace un merge de los n primeros archivos de la lista de archivos recibidos.
 * Escribe en el archivo dest. Devuelve el buffer con la cantidad de terminos
 * procesados y la cantidad de valores distintos de cero que tendrá la matriz.
 */
buffer_t* merge_lista_archivos(FILE** lista_archivos,unsigned long numero_archivos,FILE* destino){
	
	unsigned long tamanio_particion = TAM_PARTICION * 1024*1024;
	unsigned long tamanio_buffer_maximo = TAM_BUFFER * 1024*1024;
	
	cola_t** lista_colas = crear_lista_de_colas(numero_archivos);
	heap_t* terminos_con_id = heap_crear(comparar_claves);
	buffer_t* buffer = buffer_crear(destino,tamanio_buffer_maximo);
	
	llenar_colas(lista_archivos,numero_archivos,lista_colas,tamanio_particion);
	llenar_heap(terminos_con_id,lista_archivos,numero_archivos,lista_colas,tamanio_particion);
	termino_merge_t* menor_termino = NULL;
	
	while( true ) {
		
		menor_termino = obtener_menor_termino(terminos_con_id,lista_archivos,lista_colas,numero_archivos,tamanio_particion);
		
		// No hay mas archivos abiertos
		if( !menor_termino ) break;
		
		buffer_agregar_termino(buffer,menor_termino);
		
	}
	
	buffer_limpiar(buffer);
	
	printf("\n\t~~~ Terminos procesados: %lu ~~~\n",buffer_obtener_procesados(buffer));
	printf("\t~~~ Valores distintos de 0: %lu ~~~\n",buffer_obtener_distintos_de_cero(buffer));
	destruir_lista_de_colas(lista_colas,numero_archivos);
	heap_destruir(terminos_con_id,NULL);
	return buffer;
	
}

/*
 * Devuelve el término menor alfabéticamente de entre las colas recibidas.
 * Llena las colas si estas se han terminado. Recibe el máximo tamanio
 * que puede ocupar una cola en memoria. Devuelve el menor término o NULL
 * si no hay más términos en los archivos que se está procesando.
 */
termino_merge_t* obtener_menor_termino(heap_t* terminos_con_id,FILE** lista_archivos,cola_t** lista_colas,unsigned long numero_archivos,unsigned long tamanio_particion){
	
	if( heap_esta_vacio(terminos_con_id) ) return NULL;
		
	termino_merge_t* menor = NULL;
	termino_merge_t* siguiente = NULL;
	unsigned long id;
	
	menor = heap_desencolar(terminos_con_id);
	id = termino_merge_obtener_id(menor);
	if( lista_archivos && lista_colas )
		desencolar_en_heap(terminos_con_id,lista_archivos[id],lista_colas[id],tamanio_particion,id);
		
	// Chequeo de duplicados.
	while( !heap_esta_vacio(terminos_con_id) ){
		
		siguiente = (termino_merge_t*)heap_ver_max(terminos_con_id);
		
		if( strcmp(termino_merge_obtener_string(menor),termino_merge_obtener_string(siguiente))!=0 ) return menor;
	
		siguiente = heap_desencolar(terminos_con_id);
		id = termino_merge_obtener_id(siguiente);
		
		if( lista_archivos && lista_colas )
			desencolar_en_heap(terminos_con_id,lista_archivos[id],lista_colas[id],tamanio_particion,id);
			
		combinar_terminos(menor,siguiente);
		
	}
	
	return menor;
	
}

/*
 * Desencola un elemento de la cola recibido en el heap recibido. Si la cola
 * esta vacia, intenta llenarla con el archivo recibido. Si el archivo esta
 * en condicion de end of file, sale de la funcion. Si pudo desencolar un elemento
 * de la cola, se agrega este al heap.
 */
void desencolar_en_heap(heap_t* terminos_con_id,FILE* archivo,cola_t* cola,unsigned long tamanio_particion,unsigned long id){

	if( !terminos_con_id || !archivo || !cola || tamanio_particion<=0 ) return;
	
	if( cola_esta_vacia(cola) ) llenar_cola(archivo,cola,tamanio_particion,id);
	
	termino_merge_t* elemento = cola_desencolar(cola);
	
	if( !elemento ) return;
	
	heap_encolar(terminos_con_id,elemento);
	
}

/*
 * Llena de elementos (termino,aparicion,documento) la cola recibida,
 * obteniendo los mismos del archivo ordenado por termino. Llena esta
 * cola hasta llegar a un determinado tamanio en memoria.
 * Devuelve true si la pudo llenar o false en caso contrario.
 */
bool llenar_cola(FILE* archivo,cola_t* cola,unsigned long tamanio_particion,unsigned long id){
	
	if( !archivo || !cola || tamanio_particion<=0 ) return false;
		
	unsigned long tamanio_actual = 0;
	termino_merge_t* termino_merge_actual = NULL;
	char* nombre_termino = NULL;
	char* entrada = NULL;
	char* documento = NULL;
	char* str_apariciones = NULL;
	unsigned long apariciones = 0;
	
	while( !feof(archivo) && tamanio_actual<tamanio_particion ){
		
		entrada = leer_linea(archivo);
		if( !entrada ) break;
		nombre_termino = strtok(entrada,",");
		
		if( !nombre_termino ){
			free(entrada);
			break;
		}
		termino_merge_actual = termino_merge_crear(nombre_termino);
		termino_merge_set_id(termino_merge_actual,id);
		
		while(true){

			documento = strtok(NULL,",");
			if( !documento ) break;
			str_apariciones = strtok(NULL,",");
			apariciones = (unsigned long)strtof(str_apariciones,NULL);
			termino_merge_agregar_apariciones(termino_merge_actual,documento,apariciones);
			
		}
		
		tamanio_actual += termino_merge_ver_tamanio(termino_merge_actual);
		cola_encolar(cola,termino_merge_actual);
		free(entrada);
		
		// Leo la cantidad de documentos en los que aparece el termino.
		entrada = leer_linea(archivo);
		free(entrada);
		entrada = leer_linea(archivo);
		free(entrada);
		
	}
	
	return true;
	
}

/*
 * Llena la lista de n colas recibidas a partir de la lista de archivos recibidos.
 * Devuelve true si las pudo llenar a todas o false en caso contrario.
 */
void llenar_colas(FILE** lista_archivos,unsigned long numero_archivos,cola_t** lista_colas,unsigned long tamanio_particion){
	
	if( !lista_archivos || !lista_colas || tamanio_particion<=0 ) return;
	
	unsigned long i;
	
	for( i=0 ; i<numero_archivos ; i++ )
		
		llenar_cola(lista_archivos[i],lista_colas[i],tamanio_particion,i);
	
}

/*
 * Carga el heap a partir de una lista de colas. Agrega una cantidad fija
 * de elementos de cada cola (si es que esta tiene esta cantidad de elementos).
 * Si la cola se queda sin elementos, se trata de llenarla a partir de su subarchivo
 * correspondiente, recibido en la lista de archivos, por parámetro.
 */
void llenar_heap(heap_t* heap,FILE** lista_archivos,unsigned long numero_archivos,cola_t** lista_colas,unsigned long tamanio_particion){
	
	if( !heap || !lista_colas || !lista_archivos || numero_archivos<0 || tamanio_particion<0 ) return;
	
	unsigned long i,j,cantidad_por_cola;
	
	cantidad_por_cola = 10;
	
	termino_merge_t* actual;
	
	for( i=0 ; i<numero_archivos ; i++ ){
		
		if( cola_esta_vacia( lista_colas[i] ) )
			llenar_cola(lista_archivos[i],lista_colas[i],tamanio_particion,i);
		
		actual = cola_desencolar(lista_colas[i]);
		
		for( j=0 ; j<cantidad_por_cola ; j++ ){
			
			if( actual ) heap_encolar(heap,actual);
			
			actual = cola_desencolar(lista_colas[i]);
			
		}
		
		if( actual ) heap_encolar(heap,actual);
		
	}
	
}

/**	*********						********* **
 ** *********	MERGE DE DOS FASES	********* **
 ** *********						********* **/

/*
 * Realiza un merge de dos fases. El nombre del archivo final destino
 * se recibe por parametro. La cantidad de subarchivos a mergear
 * tambien se recibe por parametro. Los nombres de los subarchivos a
 * mergear deben ser siguiendo el siguiente formato:
 * subarchivo-n, donde n es un numero entre [0,cantidad).
 * Devuelve el buffer en su estado final luego de completar el merge.
 */
buffer_t* merge_dos_fases(char* merge_file,unsigned long cantidad){
	
	// Archivos destino del merge:
	FILE* dest_fase1A = fopen("output-fase-1A","w");
	FILE* dest_fase1B = fopen("output-fase-1B","w");
	FILE* dest_fase_final = fopen(merge_file,"w");
	
	unsigned long i;
	char* string_i = NULL;
	buffer_t* buffer_merge = NULL;
	
	// Determinacion de los archivos que entran en cada fase:
	unsigned long cantidad_fase1A = cantidad/2;
	unsigned long cantidad_fase1B = cantidad - cantidad_fase1A;
	
	// Lista de archivos de cada fase:
	FILE* srcs1[cantidad_fase1A];
	FILE* srcs2[cantidad_fase1B];
	
	// Apertura de archivos de la primer fase:
	for( i=0 ; i<cantidad_fase1A ; i++ ){
		string_i = (char*) malloc(24*sizeof(char));
		sprintf(string_i,"subarchivo-%lu.subarch",i);
		srcs1[i] = fopen(string_i,"r");
		free(string_i);
	}
	
	// Apertura de archivos de la segunda fase:
	for( i=0 ; i<cantidad_fase1B ; i++ ){
		string_i = (char*) malloc(24*sizeof(char));
		sprintf(string_i,"subarchivo-%lu.subarch",i+cantidad_fase1A);
		srcs2[i] = fopen(string_i,"r");
		free(string_i);
	}
	
	// Primera fase del merge:
	printf("\nFase 1A:\n");
	buffer_merge = merge_lista_archivos(srcs1,cantidad_fase1A,dest_fase1A);
	buffer_destruir(buffer_merge);
	
	// Segunda fase del merge:
	printf("\nFase 1B:\n");
	buffer_merge = merge_lista_archivos(srcs2,cantidad_fase1B,dest_fase1B);
	buffer_destruir(buffer_merge);
	
	// Cierro los archivos de ambas fases:
	fclose(dest_fase1A);
	fclose(dest_fase1B);
	for( i=0 ; i<cantidad_fase1A ; i++ ) fclose(srcs1[i]);
	for( i=0 ; i<cantidad_fase1B ; i++ ) fclose(srcs2[i]);
	
	// Abro los archivos de fases anteriores en modo lectura:
	dest_fase1A = fopen("output-fase-1A","r");
	dest_fase1B = fopen("output-fase-1B","r");
	FILE* srcs_final[] = {dest_fase1A,dest_fase1B};
	
	// Fase final del merge:
	int fases = 2;
	printf("\nFase final:\n");
	buffer_merge = merge_lista_archivos(srcs_final,fases,dest_fase_final);
	printf("\nArchivo mergeado: \"%s\"\n",merge_file);
	
	// Cierro el archivo de la fase final del merge:
	fclose(dest_fase_final);
	fclose(dest_fase1A);
	fclose(dest_fase1B);
	
	return buffer_merge;
	
}
